﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Sustainalytics.Entities.ConnectionStrings;
using MongoDB.Driver;
using Sustainalytics.Entities;
using Sustainalytics.ClientDW.Entities.Universes;
using Sustainalytics.DataLayer;
using Sustainalytics.Utils;

namespace Sustainalytics.PregenerateScreeningReports
{

  
    public  class ScreeningReportsDistributionLogic
    {
        static public IEnumerable<IEnumerable<T>> EvenPartition<T>(int partitionCount, IEnumerable<Tuple<int, T>> toDistribute)
        {
            var partitions = new Partition<T>[partitionCount];
            for (int i = 0; i < partitionCount; i++)
                  partitions[i]  = new Partition<T>{ load = 0, content = new List<T>()};


            var ordered = toDistribute.OrderByDescending(x => x.Item1);
            foreach(var input in ordered)
            {
                var dest = partitions.First(p => p.load == partitions.Min(x => x.load));
                dest.load += input.Item1;
                dest.content.Add(input.Item2);
            }
            return partitions.Select( x=>x.content).AsEnumerable();
        }    
        class Partition<T>
        {
            public int load;
            public List<T> content;
        }


        public static string RunDistribution(ScreeningToolReportDistributionSession session, 
                                                string clientDWMastercnx, string clientPlatformCnx, 
                                                string clientUniversesCnx, string clientDwCnx)
        {
            var syncContext = new StringBuilder();

            var cron = new Cronometer();

            //collect  machines 
            var machineIds = session.Machines.Select(x => x.MachineId);
            var machineIdsToStart = machineIds.Where(x => x != session.MainMachine);
            if (machineIdsToStart != null && machineIdsToStart.Any())
                AWSHelper.StartAWSInstances(machineIdsToStart/*add logging*/);
            cron.Mark("Start machines");

            // collect standard reports
            var filtersColl = MongoDatabaseFactory.GetDatabase(clientPlatformCnx).GetCollection<Filter>();
            var filters = filtersColl.Find(x => x.Type == FilterType.Standard && x.Status == FilterStatus.Active)
                                     .Project(Builders<Filter>.Projection.Include(x => x.Id).Include(x => x.Name))
                                     .As<Filter>()
                                     .ToList();
            cron.Mark("Load filters");

            // collect accounts
            var accountsColl = MongoDatabaseFactory.GetDatabase(clientPlatformCnx).GetCollection<Account>();
            var accounts = accountsColl.Find(a => a.Status == 0 && a.HasPlatformAccess)
                                .Project(Builders<Account>.Projection.Include(x => x.Id).Include(x => x.Name))
                                .As<Account>()
                                .ToList().ToDictionary(x => x.Id, x => x.Name);
            cron.Mark("Load accounts");

            // collect account -> companies
            var ids = accounts.Keys.ToArray();
            var universeCompanyColl = MongoDatabaseFactory.GetDatabase(clientUniversesCnx).GetCollection<UniverseCompany>();
            var accountContent = universeCompanyColl.Aggregate(new AggregateOptions { AllowDiskUse = true, BypassDocumentValidation = true, UseCursor = true, BatchSize = 10000 })
                    .Match(x => ids.Contains(x.AccountId))
                    .Group(x => x.AccountId, g => new { Id = g.Key, CompaniesIds = new HashSet<Guid>(g.Select(p => p.CompanyId)) })
                    .As<AccountData>()
                    .ToList();
            cron.Mark("Load accounts/companies");


            // decide distribution 
            var sb = new StringBuilder();
            var reqs = ScreeningReportsDistributionLogic.CreateDistribution(session, accounts, accountContent, sb);
            cron.Mark("Compose distribution");




            // write result to mongo ClientDWMaster
            var requestsColl = MongoDatabaseFactory.GetDatabase(clientDWMastercnx).GetCollection<ScreeningToolReportRequest>();
            
            // try clean previous requests for distribution
            requestsColl.DeleteMany(Builders<ScreeningToolReportRequest>.Filter.Where(x => x.SessionId == session.Id));            
            requestsColl.InsertMany(reqs);
            cron.Mark("Write distribution");


            // log 
            syncContext.AppendLine("Machines started:" + string.Join(", ", machineIds));
            syncContext.AppendLine("Reports :" + string.Join(", ", filters.Select(x => string.Format("({0}|{1})", x.Id, x.Name))));
            syncContext.AppendLine("Distribution: " + sb.ToString());
            syncContext.AppendLine(cron.ToString());
            return syncContext.ToString();
        }


        public static ScreeningToolReportDistributionSession CreateSession(string mainMachine, IEnumerable<string> machines,
                                                IEnumerable<Guid> reports, int ttlMinutes, Dictionary<ConnectionStringType, string> connectionStrings)
        {
            // general session object 
            var reportsArr = reports.ToList();

            var batch = (int)Math.Ceiling((double)reportsArr.Count() / (double)machines.Count());
            var machine = 0;

            var session = new ScreeningToolReportDistributionSession
            {
                Id = Guid.NewGuid(),
                Date = DateTime.UtcNow,
                Machines = machines.Select(x => new ScreeningToolReportDistributionSession.MachineData 
                { 
                    TTLMinutes  = ttlMinutes, 
                    ReportIds    =  reportsArr.Skip( (machine++) * batch).Take(batch).ToArray(), 
                    MachineId   = x 
                }),
                Completed = false,
                ConnectionStrings = connectionStrings,
                MainMachine  = mainMachine
            };

            return session;
        }



        public static IEnumerable<ScreeningToolReportRequest> CreateDistribution(
            ScreeningToolReportDistributionSession session, 
            Dictionary<Guid, string> accounts, 
            List<AccountData> accountContent, 
            StringBuilder log = null)
        {
            // do matching of accounts with themselfs to detect duplicates
            var accountsDistribution = ScreeningReportsDistributionLogic.MatchingAccounts(accountContent);

            // decide distribution 
            var machines = session.Machines.Select(x => x.MachineId);
            var reqs = CreateDistributionInternal(session.Id,  accountContent, accountsDistribution, machines);

            // prepare some logging
            if (log != null)
                ComposeLog(log, accountsDistribution, accounts, accountContent);

            return reqs;
        }

        private static List<List<Guid>> MatchingAccounts(IEnumerable<AccountData> input)
        {

            // is faster to do sort in ram than in mongo!!
            Parallel.ForEach(input, (ac) => { Array.Sort(ac.CompaniesIds); });

            var groups = input.GroupBy(x => x.CompaniesIds.Count());
            var result = new List<List<Guid>>();
            foreach (var g in groups)
            {
                var list = ((IEnumerable<AccountData>)g).ToList();

                var hasPairs = new HashSet<Guid>();
                var pairs = new List<Tuple<Guid, Guid>>();

                list.Sort((x, y) =>
                {
                    var res = CompareGuidArrays(x.CompaniesIds, y.CompaniesIds);
                    if (res == 0)
                    {
                        pairs.Add(new Tuple<Guid, Guid>(x.Id, y.Id));
                        pairs.Add(new Tuple<Guid, Guid>(y.Id, x.Id));
                        hasPairs.Add(x.Id);
                        hasPairs.Add(y.Id);
                    }
                    return res;
                });

                // singles                 
                list.ForEach(a =>
                {
                    if (!hasPairs.Contains(a.Id))
                        result.Add(new List<Guid>() { a.Id });
                });

                // handle pairs recursively
                foreach (var id in hasPairs)
                {
                    var uniqueList = new HashSet<Guid>();
                    ProcessKey(pairs, uniqueList, id);
                    if (uniqueList.Any())
                        result.Add(uniqueList.ToList());
                }
            }
            return result;
        }

        class MachineLoad
        {
            public int Load;
            public string Id;
        }
        private static IEnumerable<ScreeningToolReportRequest> CreateDistributionInternal(
            Guid sessionid, 
            List<AccountData> accountDatas, 
            List<List<Guid>> accountsMap, 
            IEnumerable<string> machines)
        {
            var dic = accountDatas.ToDictionary(x => x.Id);

            var orderedGroups = accountsMap.OrderByDescending(x => dic[x[0]].CompaniesIds.Length);
            var machinesLoad = machines.Select(m => new MachineLoad { Load = 0, Id = m }).ToList();

            // one group one request
            var result = new List<ScreeningToolReportRequest>(accountsMap.Count);
            foreach (var group in orderedGroups)
            {
                // get least loaded machine/request                
                var ml = machinesLoad.First(x => x.Load == machinesLoad.Min(y => y.Load));

                // create new request 
                var newreq = new ScreeningToolReportRequest
                {
                    Id = Guid.NewGuid(),
                    SessionId = sessionid,
                    MachineId = ml.Id,
                    AccountIds = group,
                    CompaniesIds = dic[group[0]].CompaniesIds
                };
                result.Add(newreq);

                ml.Load += newreq.CompaniesIds.Count();
            }
            return result;
        }
       

        private static  string ComposeLog(StringBuilder sb, List<List<Guid>> accountsMap, Dictionary<Guid, string> accountsDic, List<AccountData> accountDatas)
        {
            
            var dic = accountDatas.ToDictionary(x => x.Id, x => x.CompaniesIds.Length);            
            sb.AppendLine("Total accounts used:  " + accountsDic.Count + " mapped as follows:");
            sb.AppendLine("Different combinations: " + accountsMap.Count()
                          + ", Uniques: " + accountsMap.Count(x => x.Count == 1)
                          + ", Groups: " + accountsMap.Count(x => x.Count > 1));

            var total = 0.0;
            foreach (var line in accountsMap.OrderBy(x => dic[x[0]]))
            {
                var count = dic[line[0]];
                sb.Append("with " + count + " companies in account(s): ");
                if (line.Count == 1)
                {
                    sb.AppendFormat("({0}|{1}){2}", line[0], accountsDic[line[0]], System.Environment.NewLine);
                }
                else
                {
                    sb.AppendLine();
                    line.ForEach(g => sb.AppendFormat("\t({0}|{1}){2}", g, accountsDic[g], System.Environment.NewLine));
                }
                total += 4;
                total += (70) * ((double)count / 20000);
            }
            sb.AppendLine("Total estimated duration:  " + TimeSpan.FromSeconds((total * 4) / 8));
            return sb.ToString();
        }


        private static  void ProcessKey(List<Tuple<Guid, Guid>> input, HashSet<Guid> output, Guid key)
        {
            if (input.Any())  // if we still have data
            {
                // add this id
                output.Add(key);

                // search for pairs 
                var equals = input.Where(x => x.Item1 == key).Select(x => x.Item2).ToArray();

                // remove records in current input for this KEY as they are processed already
                input.RemoveAll(x => x.Item1 == key);

                // for each pair  - add to output, process from scrach  
                foreach (var e in equals)
                {
                    output.Add(e);
                    ProcessKey(input, output, e);
                }
            }
        }
        private static int CompareGuidArrays(Guid[] a, Guid[] b)
        {
            int res = a.Length - b.Length;
            for (int k = 0; k < a.Length && res == 0; k++)
                res = a[k].CompareTo(b[k]);

            return res;
        }


       
    }
}
